﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using Microsoft.Crm.Sdk.Messages;

namespace OptionSetBatchManager
{
    public partial class MainForm : Form
    {
        private IOrganizationService _service;
        // Use to persist the selected item of TreeView Node
        private OptionSetData _selectedOptionSet = null;
        // Use to persist all optionsets from Retrieve all OptionSet operation
        private List<OptionSetData> _allOptionSets = null;
        // Keep a subscriber disposable instance of any Retrieve operation
        private IDisposable _retrieveSubsciber = null;
        // Keep a subscriber disposable instance of any Load Selected OptionSet operation
        private IDisposable _loadOptionSetSubsciber = null;
        // Flag to indicate whether any execution(sorting or resetting value) is running
        private bool _isExecuting = false;

        public MainForm()
        {
            InitializeComponent();
        }
        
        private void MainForm_Load(object sender, EventArgs e)
        {
            // Launch login form 
            LoginForm loginForm = new LoginForm();
            if (loginForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                // Shows the status as it is connected to the server
                lblStatus.Text += string.Format("connected to {0}", loginForm.ServerURL);
                // Keep service context that passed the authentication
                _service = loginForm.Service;
            }
            else
            {
                Close();
            }
        }

        #region Retrieve OptionSet(s)

        private void btnRetrieveSpecificOptionSet_Click(object sender, EventArgs e)
        {
            // Unsubscribe Observable by disposing the previous if any to avoid concurrency response from multiple calls
            if (_retrieveSubsciber != null)
                _retrieveSubsciber.Dispose();

            // Clear loaded all option sets if any
            _allOptionSets = null;
            // Disable all group action buttons
            EnableDisableGroupActionButtons(false);
            // Show loading progress bar            
            ShowHideLoadingProgressBar(true);
            tvOptionSets.Nodes.Clear();

            // Create Observable object and subscribe it's callbacks
            _retrieveSubsciber = Observable.Create(new Func<IObserver<RetrieveAttributeResponse>, Action>((observer) =>
            {
                RetrieveAttributeRequest retrieveAttributeRequest = new RetrieveAttributeRequest
                {
                    EntityLogicalName = txtEntityName.Text,
                    LogicalName = txtAttributeName.Text,
                    RetrieveAsIfPublished = true
                };
                // Request RetrieveAttribute to CRM server and push the response out to the subscriber
                observer.OnNext((RetrieveAttributeResponse)_service.Execute(retrieveAttributeRequest));
                // Notice the completion to the subscriber
                observer.OnCompleted();
                return () => { };
                // Marshal operation of producing observable unit in different thread and OnNext subscribe operation in main thread (UI thread)
            })).SubscribeOn(Scheduler.Default).ObserveOn(this)
            .Subscribe(
                (retrieveAttributeResponse) =>
                {
                    // Get response object from OnNext Observable
                    PicklistAttributeMetadata optionsetAttributeMetadata = retrieveAttributeResponse.AttributeMetadata as PicklistAttributeMetadata;
                    if (optionsetAttributeMetadata != null)
                    {
                        // Create and keep OptionSetData object for the selected OptionSet
                        _selectedOptionSet = new OptionSetData(optionsetAttributeMetadata);

                        tvOptionSets.Nodes.Clear();
                        // Construct 2-levels TreeNode
                        // The first level represents an entity and second level represents child OptionSet attributes. We also keep Logical Name in Tag property for later reference.
                        TreeNode entityNode = new TreeNode(txtEntityName.Text) { Checked = true, Tag = txtEntityName.Text };
                        var attributeNode = new TreeNode(optionsetAttributeMetadata.DisplayText()) { Checked = true, Tag = txtAttributeName.Text };
                        entityNode.Nodes.Add(attributeNode);
                        tvOptionSets.Nodes.Add(entityNode);
                        // Automatically set selected node to this TreeNode
                        tvOptionSets.SelectedNode = attributeNode;
                        tvOptionSets.Focus();

                        lstOptionSetItems.Items.Clear();
                        foreach (var optionset in optionsetAttributeMetadata.OptionSet.Options)
                        {
                            lstOptionSetItems.Items.Add(new ListViewItem(new string[] { optionset.Label.UserLocalizedLabel.Label, optionset.Value.ToString() }));
                        }
                    }
                },
                (ex) => // OnError
                {
                    // Hide progressbar
                    ShowHideLoadingProgressBar(false);
                    var msg = string.Format("Retrieve specific option set failed. {0}", ex.Message);
                    WriteOutput(msg);
                    MessageBox.Show(msg, "Retrieve specific option set failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                },
                () => // OnCompleted
                {
                    // Hide progressbar
                    ShowHideLoadingProgressBar(false);
                    // Enable those action buttons for selected OptionSet
                    EnableDisableIndividualActionButtons(true);

                    tvOptionSets.ExpandAll();
                }
            );
        }

        private void btnRetrieveGlobalOptionSet_Click(object sender, EventArgs e)
        {
            // Unsubscribe Observable by disposing the previous if any to avoid concurrency response from multiple calls
            if (_retrieveSubsciber != null)
                _retrieveSubsciber.Dispose();

            // Clear loaded all option sets if any
            _allOptionSets = null;
            EnableDisableGroupActionButtons(false);

            tvOptionSets.Nodes.Clear();
            ShowHideLoadingProgressBar(true);

            // Create Observable object and subscribe it's callbacks
            _retrieveSubsciber = Observable.Create(new Func<IObserver<RetrieveOptionSetResponse>, Action>((observer) =>
            {
                RetrieveOptionSetRequest req = new RetrieveOptionSetRequest
                {
                    //my global optionset's name
                    Name = txtGlobalOptionSetName.Text,
                    RetrieveAsIfPublished = true
                };
                // Request RetrieveOptionSet to CRM server and push the response out to the subscriber
                observer.OnNext((RetrieveOptionSetResponse)_service.Execute(req));
                // Notice the completion to the subscriber
                observer.OnCompleted();
                return () => { };
                // Marshal operation of producing observable unit in different thread and OnNext subscribe operation in main thread (UI thread)
            })).SubscribeOn(Scheduler.Default).ObserveOn(this)
            .Subscribe(
                (retrieveOptionSetResponse) =>
                {
                    // Get response object from OnNext Observable
                    OptionSetMetadata retrievedOptionSetMetadata = (OptionSetMetadata)retrieveOptionSetResponse.OptionSetMetadata;

                    // Create and keep OptionSetData object for the selected OptionSet
                    _selectedOptionSet = new OptionSetData(txtGlobalOptionSetName.Text, retrievedOptionSetMetadata);

                    tvOptionSets.Nodes.Clear();
                    // Construct 2-levels TreeNode
                    // The first level represents a static text - "Global Option Sets" withoug Tag object being set so we could indicate(Parent.Tag == null) all child TreeNodes represent global OptionSet
                    TreeNode entityNode = new TreeNode("Global Option Sets") { Checked = true };
                    // The second level represents a global OptionSet and also keep the global OptionSet's name in Tag property for later reference
                    var attributeNode = new TreeNode(retrievedOptionSetMetadata.DisplayText()) { Checked = true, Tag = txtGlobalOptionSetName.Text };
                    entityNode.Nodes.Add(attributeNode);
                    tvOptionSets.Nodes.Add(entityNode);
                    // Automatically set selected node to this TreeNode
                    tvOptionSets.SelectedNode = attributeNode;
                    tvOptionSets.Focus();
                                        
                    // Get the current options list for the retrieved attribute.
                    lstOptionSetItems.Items.Clear();
                    foreach (var optionset in retrievedOptionSetMetadata.Options)
                    {
                        lstOptionSetItems.Items.Add(new ListViewItem(new string[] { optionset.Label.UserLocalizedLabel.Label, optionset.Value.ToString() }));
                    }
                },
                (ex) => // OnError
                {
                    // Hide progressbar
                    ShowHideLoadingProgressBar(false);
                    var msg = string.Format("Retrieve specific global option set failed. {0}", ex.Message);
                    WriteOutput(msg);
                    MessageBox.Show(msg, "Retrieve specific global option set failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                },
                    () => // OnCompleted
                    {
                        // Hide progressbar
                        ShowHideLoadingProgressBar(false);
                        // Enable those action buttons for selected OptionSet
                        EnableDisableIndividualActionButtons(true);                        
                        tvOptionSets.ExpandAll();
                    });
        }

        private void btnRetrieveAllOptionSets_Click(object sender, EventArgs e)
        {
            // Unsubscribe Observable by disposing the previous if any to avoid concurrency response from multiple calls
            if (_retrieveSubsciber != null)
                _retrieveSubsciber.Dispose();

            tvOptionSets.Nodes.Clear();
            ShowHideLoadingProgressBar(true);

            // Declare list of option set and clear selected
            _allOptionSets = new List<OptionSetData>();

            // Create IEnumerable sequence of RetrieveAllEntitiesResponse then convert it to Observable sequence
            // This returns two OranizationResponse in the sequence, RetrieveAllEntitiesResponse and RetrieveAllOptionSetsResponse
            _retrieveSubsciber = ActionRequest.CreateRetrieveAllOptionSetRequest(_service, chkIncludeGlobalOptionSet.Checked).ToObservable(Scheduler.Default)
                .ObserveOn(this)
                .Subscribe(
                    res => // OnNext
                    {
                        // Extract two particular responses to get the attribute-based OptionSets and global OptionSets then populate them to the TreeView and also cache them in OptionSetData datatype

                        if (res is RetrieveAllEntitiesResponse)
                        {
                            var resAllEntities = (RetrieveAllEntitiesResponse)res;
                            // Loop thru all retrieved entities
                            foreach (var entityMetadata in resAllEntities.EntityMetadata)
                            {
                                // Filter out upon the option - Only Custom Attribute 
                                var customizedOptionSetAttributes = entityMetadata.Attributes.Where(c => (!chkOnlyCustomAttribute.Checked || (c.IsCustomAttribute ?? false)) && c.AttributeType.Value == AttributeTypeCode.Picklist);
                                if (customizedOptionSetAttributes.Count() > 0)
                                {
                                    // Create tree node of entity
                                    TreeNode entityNode = new TreeNode(entityMetadata.DisplayText()) { Checked = true, Tag = entityMetadata.LogicalName };
                                    // Create tree node of option set attributes
                                    foreach (var attributeMetadata in customizedOptionSetAttributes)
                                    {
                                        entityNode.Nodes.Add(new TreeNode(attributeMetadata.DisplayText()) { Checked = true, Tag = attributeMetadata.LogicalName });
                                        _allOptionSets.Add(new OptionSetData((PicklistAttributeMetadata)attributeMetadata));
                                    }
                                    tvOptionSets.Nodes.Add(entityNode);
                                }
                            }
                        }
                        else if (res is RetrieveAllOptionSetsResponse)
                        {
                            var globalOpRes = (RetrieveAllOptionSetsResponse)res;

                            // Create tree node of entity
                            TreeNode entityNode = new TreeNode(string.Format("{0}", "Global Option Sets")) { Checked = true };
                            // Filter out upon the option - Only Custom Attribute 
                            var optionSets = chkOnlyCustomAttribute.Checked ? globalOpRes.OptionSetMetadata.Where(c => c.IsCustomOptionSet ?? false) : globalOpRes.OptionSetMetadata;
                            foreach (var optionSet in optionSets)
                            {
                                // Create tree node of option set attributes
                                entityNode.Nodes.Add(new TreeNode(optionSet.DisplayText()) { Checked = true, Tag = optionSet.Name });
                                _allOptionSets.Add(new OptionSetData(optionSet.Name, (OptionSetMetadata)optionSet));
                            }

                            tvOptionSets.Nodes.Add(entityNode);
                        }
                    },
                    (ex) => // OnError
                    {
                        // Hide progressbar
                        ShowHideLoadingProgressBar(false);
                        var msg = string.Format("Retrieve all option sets failed. {0}", ex.Message);
                        WriteOutput(msg);
                        MessageBox.Show(msg, "Retrieve all option sets failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    },
                    () => // OnCompleted
                    {
                        // Hide progressbar
                        ShowHideLoadingProgressBar(false);
                        // Enable those action buttons for group OptionSet
                        EnableDisableGroupActionButtons(true);
                        tvOptionSets.ExpandAll();
                    });
        }

        #endregion Retrieve OptionSet(s)

        #region Check/Uncheck TreeNode

        private void btnSelectAll_Click(object sender, EventArgs e)
        {
            CheckAndUncheckAll(true);
        }

        private void btnDeselectAll_Click(object sender, EventArgs e)
        {
            CheckAndUncheckAll(false);
        }

        /// <summary>
        /// Loop through all nodes and sub nodes to check or uncheck all nodes
        /// </summary>
        /// <param name="check"></param>
        private void CheckAndUncheckAll(bool check)
        {
            foreach (var node in tvOptionSets.Nodes)
            {
                ((TreeNode)node).Checked = check;
                foreach (var subNode in ((TreeNode)node).Nodes)
                {
                    ((TreeNode)subNode).Checked = check;
                }
            }
        }

        #endregion Check/Uncheck TreeNode

        #region Select TreeNode and Load OptionSet data

        private void tvOptionSets_AfterSelect(object sender, TreeViewEventArgs e)
        {
            LoadSelectedOptionSet(e.Node);            
        }

        /// <summary>
        /// Load OptionSet info from CRM server for selected item of TreeView that represent the OptionSet item
        /// </summary>
        /// <param name="node"></param>
        private void LoadSelectedOptionSet(TreeNode node)
        {
            // Only the second depth(attribute level) of TreeNode to manage
            if (node.Level == 1)
            {
                // Enable all action buttons of selected OptionSet
                EnableDisableIndividualActionButtons(true);
                // if selected node is the global option set
                if (node.Parent.Tag == null)
                {
                    RetrieveGlobalOptionSetData(node.Tag.ToString());
                }
                else // if selected node is the regular option set attribute
                {
                    RetrieveOptionSetAttributeData(node.Parent.Tag.ToString(), node.Tag.ToString());
                }
            }
        }

        /// <summary>
        /// Retrieve an attribute-based OptionSet Info from CRM server via context request
        /// </summary>
        /// <param name="entityName"></param>
        /// <param name="attributeName"></param>
        private void RetrieveOptionSetAttributeData(string entityName, string attributeName)
        {
            // Unsubscribe Observable by disposing the previous if any to avoid concurrency response from multiple calls
            if (_loadOptionSetSubsciber != null)
                _loadOptionSetSubsciber.Dispose();

            // Create Observable object and subscribe it's callbacks
            _loadOptionSetSubsciber = Observable.Create<RetrieveAttributeResponse>((observer) =>
            {                
                RetrieveAttributeRequest retrieveAttributeRequest = new RetrieveAttributeRequest
                {
                    EntityLogicalName = entityName,
                    LogicalName = attributeName,
                    RetrieveAsIfPublished = true
                };
                // Request RetrieveAttribute to CRM server and push the response out to the subscriber
                observer.OnNext((RetrieveAttributeResponse)_service.Execute(retrieveAttributeRequest));
                // Notice the completion to the subscriber
                observer.OnCompleted();
                return () => { };
                // Marshal operation of producing observable unit in different thread and OnNext subscribe operation in main thread (UI thread)
            }).SubscribeOn(Scheduler.Default).ObserveOn(this)
            .Subscribe(
                (retrieveAttributeResponse) =>
                {
                    // Get response object from OnNext Observable
                    PicklistAttributeMetadata optionsetAttributeMetadata = retrieveAttributeResponse.AttributeMetadata as PicklistAttributeMetadata;
                    if (optionsetAttributeMetadata != null)
                    {
                        // Create and keep OptionSetData object for the selected OptionSet
                        _selectedOptionSet = new OptionSetData(optionsetAttributeMetadata);
                        // Clear and populate OptionSet's items to ListView
                        lstOptionSetItems.Items.Clear();
                        foreach (var optionset in optionsetAttributeMetadata.OptionSet.Options)
                        {
                            lstOptionSetItems.Items.Add(new ListViewItem(new string[] { optionset.Label.UserLocalizedLabel.Label, optionset.Value.ToString() }));
                        }
                    }
                },
               (ex) =>
               {
                   // Inform the user if any exception occurs via both output text and messagebox
                   var msg = string.Format("Retrieve option set data failed. {0}", ex.Message);
                   WriteOutput(msg);
                   MessageBox.Show(msg, "Retrieve option set data failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
               },
                () =>
                {
                    // Do nothing for On-Completed listener as we only have a single output and we managed via OnNext
                });
        }

        /// <summary>
        /// Retrieve a global OptionSet Info from CRM server via context request
        /// </summary>
        /// <param name="globalOptionSetName"></param>
        private void RetrieveGlobalOptionSetData(string globalOptionSetName)
        {
            // Unsubscribe Observable by disposing the previous if any to avoid concurrency response from multiple calls
            if (_loadOptionSetSubsciber != null)
                _loadOptionSetSubsciber.Dispose();

            // Create Observable object and subscribe it's callbacks
            _loadOptionSetSubsciber = Observable.Create(new Func<IObserver<RetrieveOptionSetResponse>, Action>((observer) =>
            {
                RetrieveOptionSetRequest req = new RetrieveOptionSetRequest
                {
                    // global optionset's name
                    Name = globalOptionSetName
                };
                // Request RetrieveOptionSet to CRM server and push the response out to the subscriber
                observer.OnNext((RetrieveOptionSetResponse)_service.Execute(req));
                // Notice the completion to the subscriber
                observer.OnCompleted();
                return () => { };
            })).SubscribeOn(Scheduler.Default).ObserveOn(this)
            .Subscribe(
                (retrieveOptionSetResponse) =>
                {
                    // Access the retrieved OptionSetMetadata.
                    OptionSetMetadata retrievedOptionSetMetadata = (OptionSetMetadata)((RetrieveOptionSetResponse)retrieveOptionSetResponse).OptionSetMetadata;

                    _selectedOptionSet = new OptionSetData(globalOptionSetName, retrievedOptionSetMetadata);

                    // Get the current options list for the retrieved attribute.
                    lstOptionSetItems.Items.Clear();
                    foreach (var optionset in retrievedOptionSetMetadata.Options)
                    {
                        lstOptionSetItems.Items.Add(new ListViewItem(new string[] { optionset.Label.UserLocalizedLabel.Label, optionset.Value.ToString() }));
                    }
                },
               (ex) =>
               {
                   var msg = string.Format("Retrieve global option set data failed. {0}", ex.Message);
                   WriteOutput(msg);
                   MessageBox.Show(msg, "Retrieve global option set data failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
               },
                () =>
                {
                });
        }

        #endregion

        private void tvOptionSets_AfterCheck(object sender, TreeViewEventArgs e)
        {
            // Manage TreeView checkbox check behavior as following rules
            // - Checking parent means check all childs
            // - Unchecking parent mean uncheck all childs
            // - Checking any child then see if all childs are checked then check parent
            // - Unchecking any child then uncheck parent
            if (e.Action == TreeViewAction.ByMouse || e.Action == TreeViewAction.ByMouse)
            {
                if (e.Node.Level == 0)
                {
                    var isChecked = e.Node.Checked;
                    foreach (var childNode in e.Node.Nodes)
                    {
                        ((TreeNode)childNode).Checked = isChecked;
                    }
                }
                else
                {
                    bool allChecked = true;
                    foreach (var node in e.Node.Parent.Nodes)
                    {
                        if (!((TreeNode)node).Checked)
                        {
                            allChecked = false;
                            break;
                        }
                    }
                    e.Node.Parent.Checked = allChecked;
                }
            }
        }

        #region Sort Individual

        private void btnSortWithName_Click(object sender, EventArgs e)
        {
            if (_selectedOptionSet == null)
                throw new Exception("Selected Option Metadata must not be null");
            // Keep selected optionset to later compare (whether changed selection) after the operation completed
            var selectedOptionSet = _selectedOptionSet;
            // Disable all action buttons to avoid transaction lock error for multiple update requests
            EnableDisableActionButtons(false);
            // Show progressbar
            ShowHideExecutingProgressBar(true);
            WriteOutput(string.Format("Sorting selected option set '{0}' with name", selectedOptionSet));
            // Call SortOptionSets function and declare OnError and OnCompleted actions
            SortOptionSets(c => c.Label.UserLocalizedLabel.Label, selectedOptionSet,
                (ex) => OnActionError(ex, string.Format("Sorting selected option set '{0}' with name operation failed. {1}", selectedOptionSet, ex), "Sorting option set with name failed"),
                () => OnActionCompleted(selectedOptionSet, string.Format("Sorting selected option set '{0}' with name operation successfully", selectedOptionSet), "Sorting option set with name successfully")
                );
        }

        private void btnSortWithValue_Click(object sender, EventArgs e)
        {
            if (_selectedOptionSet == null)
                throw new Exception("Selected Option Metadata must not be null");
            var selectedOptionSet = _selectedOptionSet;
            // Disable all action buttons to avoid transaction lock error for multiple update requests
            EnableDisableActionButtons(false);
            ShowHideExecutingProgressBar(true);
            WriteOutput(string.Format("Sorting selected option set '{0}' with value", selectedOptionSet));
            // Call SortOptionSets function and declare OnError and OnCompleted actions
            SortOptionSets(c => c.Value, selectedOptionSet,
                (ex) => OnActionError(ex, string.Format("Sorting selected option set '{0}' with value operation failed. {1}", selectedOptionSet, ex), "Sorting option set with value failed"),
                () => OnActionCompleted(selectedOptionSet, string.Format("Sorting selected option set '{0}' with value operation successfully", selectedOptionSet), "Sorting option set with value successfully")
                );
        }

        /// <summary>
        /// Sort individual OptionSet with specified sort condition
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sortCondition">Specify what member of OptionMetadata to use for sorting</param>
        /// <param name="selectedOptionSet">The OptionSetData object to sort</param>
        /// <param name="onError">What to do when error occurs</param>
        /// <param name="onCompleted">What to do when operation completed</param>
        private void SortOptionSets<T>(Func<OptionMetadata, T> sortCondition, OptionSetData selectedOptionSet, Action<Exception> onError, Action onCompleted)
        {
            OrderOptionRequest orderReq = new OrderOptionRequest();
            if (selectedOptionSet.IsGlobal)
            {
                orderReq.OptionSetName = selectedOptionSet.GlobalOptionSetName;
            }
            else
            {
                orderReq.EntityLogicalName = selectedOptionSet.EntityName;
                orderReq.AttributeLogicalName = selectedOptionSet.AttributeName;
            }
            // Sort OptionSet items with speficied sort condition
            orderReq.Values = selectedOptionSet.OptionSetMetadata.Options.Where(c => c.Value.HasValue).OrderBy(c => sortCondition(c)).Select(c => c.Value.Value).ToArray();

            // Execute the requests to CRM server in different thread
            Observable.Start(() =>
            {
                _service.Execute(orderReq);
                // Also publish optionset if it is a global optionset
                if (selectedOptionSet.IsGlobal)
                {
                    //Publish the OptionSet
                    PublishXmlRequest pxReq3 = new PublishXmlRequest { ParameterXml = String.Format("<importexportxml><optionsets><optionset>{0}</optionset></optionsets></importexportxml>", selectedOptionSet.GlobalOptionSetName) };
                    _service.Execute(pxReq3);
                }
                else
                {
                    // Customizations must be published after an entity is updated.
                    PublishAllXmlRequest publishRequest = new PublishAllXmlRequest();
                    _service.Execute(publishRequest);
                }
            }, Scheduler.Default).ObserveOn(this).Subscribe(_ => { }, onError, onCompleted);
        }

        #endregion Sort Individual

        #region Reset Value Individual

        private void btnResetValues_Click(object sender, EventArgs e)
        {
            if (_selectedOptionSet == null)
                throw new Exception("Selected Option Metadata must not be null");
            // Keep selected optionset to later compare (whether changed selection) after the operation completed
            var selectedOptionSet = _selectedOptionSet;
            var initValue = (int)numBeginWith.Value;
            // Disable all action buttons to avoid transaction lock error for multiple update requests
            EnableDisableActionButtons(false);
            ShowHideExecutingProgressBar(true);

            WriteOutput(string.Format("Resetting values of option set '{0}' with startup value of {1}", selectedOptionSet, initValue));

            ActionRequest.CreateResetValueRequest(_service, selectedOptionSet, initValue).ToObservable(Scheduler.Default).ObserveOn(this)
                .Subscribe(
                    WriteOutput,
                    (ex) => OnActionError(ex, string.Format("Reset values of option set '{0}' failed. {1}", selectedOptionSet, ex.Message), "Reset Value failed"),
                    () => OnActionCompleted(selectedOptionSet, string.Format("Reset values of option set '{0}' successfully", selectedOptionSet), "Reset Value successfully")
                );
        }

        #endregion

        #region Group Actions

        private void btnGroupResetValues_Click(object sender, EventArgs e)
        {
            if (_allOptionSets == null)
                throw new Exception("All OptionSets Metadata must not be null");
            var initValue = (int)numGroupBeginWith.Value;
            // Keep selected optionset to later compare (whether changed selection) after the operation completed
            var selectedOptionSet = _selectedOptionSet;
            btnCancel.Visible = true;
            EnableDisableActionButtons(false);
            ShowHideExecutingProgressBar(true);
            
            WriteOutput(string.Format("Resetting values of all option sets with startup value of {0}", initValue));
            // Make sequence request thru Observable sequence
            ActionRequest.CreateResetValueRequest(_service, OnlyCheckedOptionSets(), initValue).ToObservable(Scheduler.Default).ObserveOn(this)
                .Subscribe(
                    WriteOutput,
                    (ex) => OnActionError(ex, string.Format("Reset values of all option sets failed. {0}", ex.Message), "Reset Value failed"),
                    () =>
                    {
                        OnActionCompleted(selectedOptionSet, "Reset values of all option sets successfully.", "Reset Value successfully");
                        ReloadAllOptionSetsAfterActionCompleted();
                    }
                );
        }

        private void btnGroupSortWithName_Click(object sender, EventArgs e)
        {
            SortOptionSets(c => c.Label.UserLocalizedLabel.Label, "name");
        }

        private void btnGroupSortWithValue_Click(object sender, EventArgs e)
        {
            SortOptionSets(c => c.Value, "value");
        }

        /// <summary>
        /// Sort all selected OptionSets with specified sort condition
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sortCondition"></param>
        /// <param name="sortWith"></param>
        private void SortOptionSets<T>(Func<OptionMetadata, T> sortCondition, string sortWith)
        {
            if (_allOptionSets == null)
                throw new Exception("All OptionSets Metadata must not be null");
            var selectedOptionSet = _selectedOptionSet;

            EnableDisableActionButtons(false);
            ShowHideExecutingProgressBar(true);

            WriteOutput(string.Format("Sorting selected option set '{0}' with {1}", selectedOptionSet, sortWith));

            ActionRequest.CreateOrderRequest(_service, OnlyCheckedOptionSets(), sortCondition)
                .ToObservable(Scheduler.Default)
                .ObserveOn(this)
                .Subscribe(
                    WriteOutput,
                    (ex) => OnActionError(ex, string.Format("Sorting all option sets with {0} operation failed. {1}", sortWith, ex.Message), string.Format("Sorting all option sets failed", sortWith)),
                    () =>
                    {
                        OnActionCompleted(selectedOptionSet, string.Format("Sorting all option sets with {0} operation successfully", sortWith), string.Format("Sorting all option sets with {0} successfully", sortWith));
                        ReloadAllOptionSetsAfterActionCompleted();
                    }
                );
        }

        private void ReloadAllOptionSetsAfterActionCompleted()
        {
            if (_allOptionSets != null)
            {
                if (MessageBox.Show("Reload all OptionSets is recommended if you need to continue managing them. Would you like to re-retrieve all OptionSets?", "Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == System.Windows.Forms.DialogResult.Yes)
                    btnRetrieveAllOptionSets_Click(null, EventArgs.Empty);
            }
        }

        #endregion Group Actions

        private void OnActionError(Exception ex, string outputMessage, string messageBoxCaption)
        {
            // Hide ProgressBar
            ShowHideExecutingProgressBar(false);
            // Enable back all action buttons
            EnableDisableActionButtons(true);
            btnCancel.Visible = false;
            WriteOutput(outputMessage);
            MessageBox.Show(outputMessage, messageBoxCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);
        }

        private void OnActionCompleted(OptionSetData selectedOptionSetData, string outputMessage, string messageBoxCaption)
        {
            // Hide ProgressBar
            ShowHideExecutingProgressBar(false);
            // Enable back all action buttons
            EnableDisableActionButtons(true);
            btnCancel.Visible = false;
            WriteOutput(outputMessage);
            MessageBox.Show(outputMessage, messageBoxCaption, MessageBoxButtons.OK, MessageBoxIcon.Information);
            // If the current selected TreeView node still equals to the cache one, meaning that user yet change selection 
            // then re-load the OptionSets to the ListView with reflected data
            if (selectedOptionSetData == _selectedOptionSet && tvOptionSets.SelectedNode != null)
            {
                LoadSelectedOptionSet(tvOptionSets.SelectedNode);
            }
        }

        /// <summary>
        /// Filter to collect only checked optionsets
        /// </summary>
        /// <returns></returns>
        private List<OptionSetData> OnlyCheckedOptionSets()
        {
            var filteredOptionSetDatas = new List<OptionSetData>();
            foreach (var node in tvOptionSets.Nodes)
            {
                foreach (var subNode in ((TreeNode)node).Nodes)
                {
                    var treeNode = (TreeNode)subNode;
                    if (treeNode.Checked)
                    {
                        OptionSetData checkedOptionSetData = null;
                        if (treeNode.Parent.Tag == null)
                        {
                            checkedOptionSetData = _allOptionSets.SingleOrDefault(c => c.IsGlobal && c.GlobalOptionSetName == treeNode.Tag.ToString());
                        }
                        else
                        {
                            checkedOptionSetData = _allOptionSets.SingleOrDefault(c => c.EntityName == treeNode.Parent.Tag.ToString() && c.AttributeName == treeNode.Tag.ToString());
                        }

                        if (checkedOptionSetData == null)
                            throw new InvalidOperationException(string.Format("Could not find OptionSet data for some of the checked items."));

                        filteredOptionSetDatas.Add(checkedOptionSetData);
                    }
                }
            }

            return filteredOptionSetDatas;
        }

        private void WriteOutput(string message)
        {
            txtOutput.AppendText(DateTime.Now.ToString("hh:mm:ss") + " - " + message + Environment.NewLine);
        }
        
        private void EnableDisableActionButtons(bool enable)
        {
            _isExecuting = !enable;
            btnGroupResetValues.Enabled = _allOptionSets != null? enable: false;
            btnResetValues.Enabled = _selectedOptionSet != null ? enable : false;
            btnGroupSortWithName.Enabled = _allOptionSets != null ? enable : false;
            btnSortWithName.Enabled = _selectedOptionSet != null ? enable : false;
            btnGroupSortWithValue.Enabled = _allOptionSets != null ? enable : false;
            btnSortWithValue.Enabled = _selectedOptionSet != null ? enable : false;
        }

        private void EnableDisableGroupActionButtons(bool enable)
        {
            if (!_isExecuting)
            {
                btnGroupResetValues.Enabled = enable;
                btnGroupSortWithValue.Enabled = enable;
                btnGroupSortWithName.Enabled = enable;
            }
        }

        private void EnableDisableIndividualActionButtons(bool enable)
        {
            if (!_isExecuting)
            {
                btnResetValues.Enabled = enable;
                btnSortWithName.Enabled = enable;
                btnSortWithValue.Enabled = enable;
            }
        }

        private void ShowHideLoadingProgressBar(bool show)
        {
            lblLoading.Visible = show;
            progressBarLoading.Visible = show;
        }

        private void ShowHideExecutingProgressBar(bool show)
        {
            lblExecuting.Visible = show;
            progressBarExecuting.Visible = show;
        }

        private void btnCancel_ButtonClick(object sender, EventArgs e)
        {
            ActionRequest.CancelOperation();
        }
    }
}
